package org.xx.armory.ofs.controllers;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.NameScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import org.xx.armory.commons.ForLogging;
import org.xx.armory.ofs.FileRef;
import org.xx.armory.ofs.jpa.FileRefPO;
import org.xx.armory.ofs.jpa.FileRefPO_;
import org.xx.armory.ofs.jpa.FileRefRepository;
import org.xx.armory.spring5.data.PageArgs;
import org.xx.armory.spring5.mvc.ResourceNotFoundException;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.List;

import static java.util.Collections.emptyList;
import static org.apache.commons.io.FilenameUtils.getExtension;
import static org.apache.commons.io.FilenameUtils.getName;
import static org.apache.commons.io.IOUtils.copyLarge;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.xx.armory.commons.DebugUtils.dumpTraceBack;
import static org.xx.armory.commons.ResponseUtils.contentType;
import static org.xx.armory.commons.ResponseUtils.encodeRFC2231;
import static org.xx.armory.commons.Validators.rejectIfNegative;
import static org.xx.armory.commons.Validators.rejectIfNonPositive;
import static org.xx.armory.ofs.FileRefUtils.createUuid;
import static org.xx.armory.ofs.FileRefUtils.hashFile;
import static org.xx.armory.spring5.mvc.ModelUtils.buildJsonView;

@Controller
@RequestMapping("/")
public class FileController {
    private final static String ACTION_INFO = "info";
    private final static String ACTION_DOWNLOAD = "download";

    @ForLogging
    private final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileRefRepository fileRefRepository;

    @Autowired
    private FileSystemManager fileSystemManager;

    @Value("${vfs.root-path}")
    private String rootPath;

    private List<FileRef> findAllByObjectIdAndFileType(
            long objectId,
            int fileType
    ) {
        return this.fileRefRepository
                .findAllByObjectIdAndFileType(
                        objectId,
                        fileType,
                        PageArgs.unpaged().asc(FileRefPO_.lastModifiedTime.getName())
                ).map(FileRefRepository::toFileRef)
                .toList();
    }

    @GetMapping(value = "/root",
                produces = {"text/html"})
    @Transactional(readOnly = true)
    public ModelAndView getRootHtml(
            @RequestParam(value = "object-id",
                          defaultValue = "0") long objectId,
            @RequestParam(value = "file-type",
                          defaultValue = "0") int fileType
    ) {
        rejectIfNegative(objectId, "objectId");
        rejectIfNegative(fileType, "fileType");

        final ModelAndView result = new ModelAndView("/root/index");

        result.addObject("objectId", objectId);
        result.addObject("fileType", fileType);
        result.addObject("fileRefList", objectId != 0 && fileType != 0 ? findAllByObjectIdAndFileType(objectId, fileType) : emptyList());

        return result;
    }

    @GetMapping("/root")
    @Transactional(readOnly = true)
    public ModelAndView getRoot(
            @RequestParam(value = "object-id",
                          defaultValue = "0") long objectId,
            @RequestParam(value = "file-type",
                          defaultValue = "0") int fileType
    ) {
        rejectIfNonPositive(objectId, "objectId");
        rejectIfNonPositive(fileType, "fileType");

        return buildJsonView(findAllByObjectIdAndFileType(objectId, fileType));
    }

    private FileRefPO createNew(
            long objectId,
            int fileType,
            String fileName
    ) {
        final var result = new FileRefPO();

        result.setObjectId(objectId);
        result.setFileType(fileType);
        result.setName(fileName);

        return result;
    }

    @PostMapping("/root")
    @Transactional
    public ModelAndView postRoot(
            @RequestParam("object-id") long objectId,
            @RequestParam("file-type") int fileType,
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "description",
                          defaultValue = "") String description
    )
            throws IOException {
        final var fileName = getName(file.getOriginalFilename());

        logger.debug("Upload file(object-id={}, file-type={}, file-name={}, file-size={}, description={})",
                     objectId, fileType, file.getOriginalFilename(), file.getSize(), description);

        final var fileRefPO = this.fileRefRepository
                .findByObjectIdAndFileTypeAndName(objectId, fileType, fileName)
                .orElseGet(() -> createNew(objectId, fileType, fileName));

        var uuid = fileRefPO.getUuid();
        if (isBlank(uuid)) {
            uuid = createUuid().toString();
        }

        final var fileObject = resolve(objectId, fileType, uuid);
        if (fileObject == null || (fileObject.exists() && !fileObject.isWriteable())) {
            throw new ResourceNotFoundException();
        }

        try (final var inputStream = file.getInputStream();
             final var outputStream = fileObject.getContent().getOutputStream(false)) {
            copyLarge(inputStream, outputStream);
            outputStream.flush();
        }

        fileRefPO.setSize(file.getSize());
        fileRefPO.setDescription(description);
        fileRefPO.setUuid(uuid);

        this.fileRefRepository.saveAndFlush(fileRefPO);

        return buildJsonView(FileRefRepository.toFileRef(fileRefPO));
    }

    @GetMapping("/{id:\\d+}/{hash:[a-zA-Z0-9_]+}")
    @Transactional
    public ModelAndView get(
            HttpServletResponse httpResponse,
            @PathVariable("id") long id,
            @PathVariable("hash") String hash,
            @RequestParam(value = "action",
                          defaultValue = "") String action,
            @RequestHeader(value = "If-Modified-Since",
                           required = false) Date ifModifiedSince
    )
            throws IOException {
        final var action_1 = trimToEmpty(action).toLowerCase();

        final var fileRefPO = this.fileRefRepository.findById(id)
                                                    .filter(fileRef_ -> hashFile(fileRef_.getId(), fileRef_.getName()).equals(hash))
                                                    .orElseThrow(ResourceNotFoundException::new);

        if (ifModifiedSince != null && fileRefPO.getLastModifiedTime().getTime() <= ifModifiedSince.getTime()) {
            // 修改日期早于或者等于失效日期，应当返回 NOT_MODIFIED。
            httpResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            return null;
        }

        httpResponse.addDateHeader("Last-Modified", fileRefPO.getLastModifiedTime().getTime());

        // 下载文件。
        // 返回文件内容。
        if (action_1.equals(ACTION_INFO)) {
            // 返回文件信息。
            return buildJsonView(FileRefRepository.toFileRef(fileRefPO));
        } else download(httpResponse, fileRefPO, action_1.equals(ACTION_DOWNLOAD));

        return null;
    }

    @DeleteMapping("/{id:\\d+}/{hash:[a-zA-Z0-9_]+}")
    @Transactional
    public ModelAndView unlink(
            @PathVariable("id") long id,
            @PathVariable("hash") String hash
    )
            throws IOException {
        final var fileRefPO = this.fileRefRepository.findById(id)
                                                    .filter(fileRef_ -> hashFile(fileRef_.getId(), fileRef_.getName()).equals(hash))
                                                    .orElseThrow(ResourceNotFoundException::new);

        this.fileRefRepository.delete(fileRefPO);

        final var fileObject = resolve(fileRefPO.getObjectId(), fileRefPO.getFileType(), fileRefPO.getUuid());
        final var result = fileObject == null || fileObject.delete();

        logger.debug("Delete {}: {}", fileObject, result);

        return buildJsonView(result);
    }

    private void download(
            HttpServletResponse httpResponse,
            FileRefPO fileRefPO,
            boolean attachment
    )
            throws IOException {
        final var now = new Date();
        final var ext = "." + getExtension(fileRefPO.getName());
        var fileContentType = contentType(ext);

        httpResponse.addDateHeader("Date", now.getTime());
        if (fileContentType.startsWith("text/")) {
            // 文本类型。
            httpResponse.addHeader("Content-Type", fileContentType + ";charset=UTF-8");
        } else {
            httpResponse.addHeader("Content-Type", fileContentType);
        }
        httpResponse.addHeader("Content-Length", String.valueOf(fileRefPO.getSize()));
        if (attachment) {
            httpResponse.addHeader("Content-Disposition", "attachment;filename*=" + encodeRFC2231(fileRefPO.getName()));
        }

        final var fileObject = resolve(fileRefPO.getObjectId(), fileRefPO.getFileType(), fileRefPO.getUuid());
        if (fileObject == null || !fileObject.isReadable()) {
            logger.warn("Cannot read file-object {} as content of {}", fileObject, fileRefPO);
            throw new ResourceNotFoundException(fileRefPO.getId());
        }

        fileObject.getContent().write(httpResponse.getOutputStream());
        httpResponse.flushBuffer();
    }

    private FileObject getRootFileObject()
            throws FileSystemException {
        return this.fileSystemManager.resolveFile(this.rootPath);
    }

    /**
     * 根据文件引用获取文件对象。
     *
     * @param objectId
     *         关联的对象的ID。
     * @param fileType
     *         关联的文件类型。
     * @param fileName
     *         文件名。
     * @return 解析结果，如果参数 {@code fileName}是 {@code null} 或者只包含空白字符，那么返回 {@code null}。
     */
    private FileObject resolve(
            long objectId,
            int fileType,
            String fileName
    ) {
        if (isBlank(fileName)) {
            return null;
        }

        final var path = fileType + "/" + objectId + "/" + fileName;

        // 然后通过根路径解析为对应的文件对象。
        try {
            return getRootFileObject().resolveFile(path, NameScope.DESCENDENT_OR_SELF);
        } catch (FileSystemException ex) {
            logger.error("Cannot resolve file object: {}\n{}", path, dumpTraceBack(ex));
            return null;
        }
    }

}
